import os
import sys
import glob
import webbrowser
from pathlib import Path
from typing import Any, Dict, List, Optional, Tuple

from urllib.parse import quote_plus

import docx
import re
import shutil

from .config import Config
from .llm import DoubaoLLM
from .logger import log_event
from .browser import search as browser_search, navigate as browser_navigate, click_first_result, click_link_text, close_driver
from .vision import VisionEngine
from .tts import SpeechFeedback


class ActionExecutor:
    def __init__(self, llm: Optional[DoubaoLLM] = None):
        self.llm = llm or DoubaoLLM()
        # 统一的视觉步骤重试与间隔参数
        try:
            from .config import CV_CLICK_RETRY, CV_SEARCH_TIMEOUT_SEC
            self.visual_retry_max = max(int(CV_CLICK_RETRY), 2)
            self.visual_search_timeout_sec = int(CV_SEARCH_TIMEOUT_SEC)
        except Exception:
            self.visual_retry_max = 3
            self.visual_search_timeout_sec = 6
        self.visual_interval_sec = 0.6

    # 通用“视觉守护”步骤执行器：执行-校验-重试直到成功或超限
    def _run_visual_step(
        self,
        name: str,
        do_fn,
        verify_fn,
        precheck=None,
        max_retries: Optional[int] = None,
        interval_sec: Optional[float] = None,
    ) -> bool:
        import time
        retries = int(max_retries if max_retries is not None else getattr(self, "visual_retry_max", 3))
        interval = float(interval_sec if interval_sec is not None else getattr(self, "visual_interval_sec", 0.6))
        try:
            log_event("VIS_STEP_BEGIN", f"name={name}, retries={retries}, interval={interval}")
        except Exception:
            pass
        # 操作前视觉分析：若已满足目标态，则直接成功
        if precheck:
            try:
                if precheck():
                    try:
                        log_event("VIS_STEP_SKIP", f"name={name}, reason=precheck_satisfied")
                    except Exception:
                        pass
                    return True
            except Exception:
                pass
        # 执行-校验-重试
        for i in range(retries):
            try:
                do_fn()
            except Exception as e:
                try:
                    log_event("VIS_STEP_DO_ERR", f"name={name}, attempt={i+1}, error={e}")
                except Exception:
                    pass
            time.sleep(interval)
            ok = False
            try:
                ok = bool(verify_fn())
            except Exception as e:
                try:
                    log_event("VIS_STEP_VERIFY_ERR", f"name={name}, attempt={i+1}, error={e}")
                except Exception:
                    pass
            if ok:
                try:
                    log_event("VIS_STEP_SUCCESS", f"name={name}, attempt={i+1}")
                except Exception:
                    pass
                return True
            try:
                log_event("VIS_STEP_RETRY", f"name={name}, attempt={i+1}")
            except Exception:
                pass
            time.sleep(interval)
        try:
            log_event("VIS_STEP_FAIL", f"name={name}, retries={retries}")
        except Exception:
            pass
        return False

    def execute_actions(self, actions: List[Dict[str, Any]]) -> List[Dict[str, Any]]:
        results: List[Dict[str, Any]] = []
        try:
            SpeechFeedback().speak(f"开始执行任务，共{len(actions)}个步骤。")
        except Exception:
            pass
        for action in actions:
            t = action.get("type")
            log_event("ACTION_EXECUTE", f"type={t}, payload={action}")
            try:
                if t == "play_music":
                    ok, detail = self.play_music(action.get("song_name", ""))
                elif t == "write_article":
                    ok, detail = self.write_article(
                        topic=action.get("topic", "主题"),
                        words=int(action.get("words", 300)),
                        save_path=action.get("save_path", "Desktop"),
                    )
                elif t == "file_op":
                    ok, detail = self._execute_file_op(
                        op=action.get("op", ""),
                        src=action.get("src", ""),
                        dest=action.get("dest", ""),
                        name=action.get("name", ""),
                    )
                elif t == "app_control":
                    ok, detail = self._execute_app_control(
                        op=action.get("op", ""),
                        app=action.get("app", ""),
                    )
                elif t == "system_control":
                    ok, detail = self._execute_system_control(
                        op=action.get("op", ""),
                        value=action.get("value", None),
                        target=action.get("target", None),
                    )
                elif t == "system":  # 兼容配置中的别名
                    ok, detail = self._execute_system_control(
                        op=action.get("op", ""),
                        value=action.get("value", None),
                        target=action.get("target", None),
                    )
                elif t == "web_search":
                    ok, detail = self.web_search(action.get("query", ""))
                elif t == "navigate":
                    ok, detail = self.navigate(action.get("url", ""))
                elif t == "browser_click":
                    ok, detail = self.browser_click(action.get("target", ""), action.get("value"))
                elif t == "wechat_message":
                    ok, detail = self._execute_wechat_message(
                        contact=action.get("contact", ""),
                        text=action.get("text", ""),
                    )
                elif t == "qq_message":
                    ok, detail = self._execute_qq_message(
                        contact=action.get("contact", ""),
                        text=action.get("text", ""),
                    )
                elif t == "needs_clarification":
                    ok, detail = False, action.get("question", "需要进一步澄清指令")
                else:
                    ok, detail = False, f"未知动作类型: {t}"
            except Exception as e:
                ok, detail = False, f"执行失败: {e}"
            results.append({"action": t, "success": ok, "detail": detail})
            log_event("ACTION_RESULT", f"type={t}, success={ok}, detail={detail}")
        try:
            SpeechFeedback().speak("任务执行完成。")
        except Exception:
            pass
        return results

    def play_music(self, song_name: str) -> Tuple[bool, str]:
        song_name = song_name.strip()
        if not song_name:
            return False, "未指定歌曲名称"

        # Try local files first
        local = self._find_local_music(song_name)
        if local:
            try:
                os.startfile(local)  # Windows: open with default player
                return True, f"已打开本地音乐文件: {local}"
            except Exception as e:
                pass

        # Fallback: open web search (NetEase or Baidu)
        try:
            q = quote_plus(song_name + " 音乐 在线 播放")
            url = f"https://www.baidu.com/s?wd={q}"
            webbrowser.open(url)
            return True, "已在浏览器打开在线播放页面"
        except Exception as e:
            return False, f"无法打开在线播放: {e}"

    def _find_local_music(self, song_name: str) -> Optional[str]:
        candidates: List[Path] = []
        home = Path.home()
        common_dirs = [
            home / "Music",
            home / "Downloads",
            home / "Desktop",
            Path("C:/Users/Public/Music"),
        ]
        exts = ["*.mp3", "*.flac", "*.wav", "*.m4a", "*.aac"]

        for d in common_dirs:
            if d.exists():
                for ext in exts:
                    candidates.extend(Path(d).glob(f"**/{ext}"))
        # Match by substring
        s = song_name.lower()
        for p in candidates:
            if s in p.name.lower():
                return str(p)
        return None

    def write_article(self, topic: str, words: int, save_path: str) -> Tuple[bool, str]:
        content = self.llm.generate_article_content(topic, words)
        title = topic.strip() or "文章"
        # Resolve save directory (allow Default to use configuration)
        save_dir = self._resolve_save_dir(save_path)
        save_dir.mkdir(parents=True, exist_ok=True)
        file_name = f"{title}_{words}字.docx"
        file_path = save_dir / file_name
        doc = docx.Document()
        doc.add_heading(title, level=1)
        doc.add_paragraph(content)
        doc.save(str(file_path))
        return True, f"文章已保存: {file_path}"

    # New: Web actions
    def web_search(self, query: str) -> Tuple[bool, str]:
        query = (query or "").strip()
        if not query:
            return False, "未指定搜索关键词"
        engine = getattr(Config, "SEARCH_ENGINE", "baidu")
        ok = browser_search(query, engine=engine)
        if ok:
            return True, f"已在{engine}搜索：{query}"
        try:
            q = quote_plus(query)
            url = f"https://www.baidu.com/s?wd={q}" if engine == "baidu" else f"https://www.bing.com/search?q={q}"
            webbrowser.open(url)
            return True, "已在浏览器打开搜索结果页"
        except Exception as e:
            return False, f"搜索失败: {e}"

    def navigate(self, url: str) -> Tuple[bool, str]:
        url = (url or "").strip()
        if not url:
            return False, "未指定导航地址"
        ok = browser_navigate(url)
        if ok:
            return True, f"已导航到：{url}"
        try:
            webbrowser.open(url)
            return True, f"已在浏览器打开：{url}"
        except Exception as e:
            return False, f"打开失败: {e}"

    def browser_click(self, target: str, value: Optional[str] = None) -> Tuple[bool, str]:
        tgt = (target or "").strip()
        engine = getattr(Config, "SEARCH_ENGINE", "baidu")
        if tgt == "first_result":
            ok = click_first_result(engine=engine)
            return (True, "已点击第一条搜索结果") if ok else (False, "点击第一条结果失败")
        if tgt == "link_text":
            if not value:
                return False, "未提供链接文本"
            ok = click_link_text(value)
            return (True, f"已点击链接：{value}") if ok else (False, f"未找到链接：{value}")
        return False, f"未知点击目标：{tgt}"

    def _resolve_save_dir(self, save_path: str) -> Path:
        # Use configured default when not provided or explicitly set to Default/默认
        sp = (save_path or Config.DEFAULT_SAVE_DIR).strip()
        if sp.lower() in ("default", "默认"):
            sp = Config.DEFAULT_SAVE_DIR
        home = Path.home()
        if sp.lower() in ("desktop", "桌面"):
            return home / "Desktop"
        if sp.lower() in ("documents", "文档", "我的文档"):
            return home / "Documents"
        # E drive keywords
        if sp in ("E:/", "E\\", "E:", "E盘", "E 盘", "E盘下"):
            return Path("E:/")
        # Absolute Windows drive letter path
        if re.match(r"^[A-Za-z]:[\\/].*", sp) or re.match(r"^[A-Za-z]:$", sp):
            return Path(sp)
        p = Path(sp)
        if p.suffix.lower() == ".docx":
            return p.parent
        return p if p.is_absolute() else Path.cwd() / p

    def _resolve_path_hint(self, hint: str) -> Path:
        """Resolve a path or keyword like Desktop/Documents/E:/ to a concrete Path.
        If it's a filename without path, resolve relative to current working directory.
        """
        if not hint:
            return Path.cwd()
        sp = hint.strip()
        # Reuse save dir resolver for directory-like keywords
        if sp.lower() in ("desktop", "桌面", "documents", "文档", "我的文档", "default", "默认", "e:/", "e\\", "e:", "e盘", "e 盘", "e盘下"):
            return self._resolve_save_dir(sp)
        # Absolute path
        if re.match(r"^[A-Za-z]:[\\/].*", sp) or re.match(r"^[A-Za-z]:$", sp):
            return Path(sp)
        p = Path(sp)
        return p if p.is_absolute() else Path.cwd() / p

    def _execute_file_op(self, op: str, src: str, dest: str = "", name: str = "") -> Tuple[bool, str]:
        op = (op or "").strip().lower()
        if op == "create":
            # src can be a dir hint, or a full path. name optionally provided.
            if name:
                base = self._resolve_path_hint(src)
                base.mkdir(parents=True, exist_ok=True)
                target = base / name
            else:
                target = self._resolve_path_hint(src)
                target.parent.mkdir(parents=True, exist_ok=True)
            try:
                target.touch(exist_ok=True)
                return True, f"已创建文件: {target}"
            except Exception as e:
                return False, f"创建失败: {e}"

        if op == "delete":
            target = self._resolve_path_hint(src)
            try:
                try:
                    from send2trash import send2trash  # type: ignore
                    send2trash(str(target))
                    return True, f"已移动到回收站: {target}"
                except Exception:
                    if target.is_file():
                        target.unlink(missing_ok=True)
                    elif target.is_dir():
                        shutil.rmtree(target, ignore_errors=True)
                    return True, f"已删除: {target}"
            except Exception as e:
                return False, f"删除失败: {e}"

        if op == "move":
            s = self._resolve_path_hint(src)
            d = self._resolve_path_hint(dest)
            try:
                d.parent.mkdir(parents=True, exist_ok=True)
                shutil.move(str(s), str(d))
                return True, f"已移动: {s} -> {d}"
            except Exception as e:
                return False, f"移动失败: {e}"

        if op == "copy":
            s = self._resolve_path_hint(src)
            d = self._resolve_path_hint(dest)
            try:
                d_parent = d if d.suffix else d.parent
                d_parent.mkdir(parents=True, exist_ok=True)
                if s.is_dir():
                    shutil.copytree(str(s), str(d), dirs_exist_ok=True)
                else:
                    shutil.copy2(str(s), str(d))
                return True, f"已复制: {s} -> {d}"
            except Exception as e:
                return False, f"复制失败: {e}"

        if op == "rename":
            s = self._resolve_path_hint(src)
            d = self._resolve_path_hint(dest)
            try:
                # If dest looks like just a name (no drive, no slash), keep source directory
                dest_str = (dest or "").strip()
                is_name_only = (":" not in dest_str) and ("/" not in dest_str) and ("\\" not in dest_str)
                if is_name_only:
                    d = s.with_name(dest_str)
                else:
                    # ensure parent exists when specifying a full path
                    if not d.exists() and (not d.parent.exists()):
                        d.parent.mkdir(parents=True, exist_ok=True)
                s.rename(d)
                return True, f"已重命名: {s} -> {d}"
            except Exception as e:
                return False, f"重命名失败: {e}"

        if op == "list":
            base = self._resolve_path_hint(src)
            if not base.exists() or not base.is_dir():
                return False, f"目录不存在: {base}"
            try:
                entries = list(base.iterdir())
                names = ", ".join(p.name for p in entries[:50])
                ellipsis = " ..." if len(entries) > 50 else ""
                return True, f"{base} 包含 {len(entries)} 项: {names}{ellipsis}"
            except Exception as e:
                return False, f"列出失败: {e}"

        return False, f"未知文件操作: {op}"

    def _execute_app_control(self, op: str, app: str) -> Tuple[bool, str]:
        op = (op or "").strip().lower()
        app = (app or "").strip()
        if not app:
            return False, "未指定应用"
        # 拆分长串，优先使用第一个有效候选
        tokens = [t.strip() for t in re.split(r"[，、,；;\s]+", app) if t.strip()]
        primary = tokens[0] if tokens else app

        # 解析别名（支持套件），先查配置与模糊匹配，再使用 LLM 推断
        exe = Config.APP_ALIASES.get(primary)
        matched_alias = None
        if exe is None:
            for tkn in tokens[1:]:
                if tkn in Config.APP_ALIASES:
                    matched_alias = tkn
                    exe = Config.APP_ALIASES.get(matched_alias)
                    break
        if exe is None:
            try:
                from difflib import get_close_matches
                candidates = list(Config.APP_ALIASES.keys())
                matches = get_close_matches(primary, candidates, n=1, cutoff=0.6)
                if not matches:
                    for tkn in tokens[1:]:
                        m2 = get_close_matches(tkn, candidates, n=1, cutoff=0.6)
                        if m2:
                            matches = m2
                            primary = tkn
                            break
                if matches:
                    matched_alias = matches[0]
                    exe = Config.APP_ALIASES.get(matched_alias)
                    try:
                        log_event("APP_FUZZY_MATCH", f"input={app}, primary={primary}, matched={matched_alias}")
                    except Exception:
                        pass
            except Exception:
                pass
        if exe is None:
            # LLM 别名推断（可能直接返回可执行文件名，如 WeChat.exe）
            try:
                guess = self.llm.infer_alias(primary)
                if guess:
                    matched_alias = matched_alias or guess
                    exe = guess
                    log_event("APP_ALIAS_LLM", f"input={app}, guess={guess}")
            except Exception:
                pass
        exe = exe or primary
        try:
            log_event("APP_ALIAS_RESOLVE", f"input={app}, resolved={exe}")
        except Exception:
            pass

        # 工具函数：进程存活检测与结束
        import subprocess
        def _is_running(name: str) -> bool:
            try:
                out = subprocess.run(["tasklist", "/FI", f"IMAGENAME eq {name}"], capture_output=True, text=True, check=False)
                return name.lower() in (out.stdout or "").lower()
            except Exception:
                return False
        def _kill(name: str, force: bool = False) -> None:
            args = ["taskkill", "/IM", name]
            if force:
                args.append("/F")
            try:
                log_event("APP_CLOSE_ATTEMPT", f"name={name}, force={force}")
                subprocess.run(args, check=False)
            except Exception:
                pass

        # 套件支持：WPS
        suite_targets: List[str] = []
        if isinstance(exe, str) and exe.lower().startswith("suite:"):
            suite_name = exe.split(":", 1)[1].lower()
            if suite_name == "wps":
                suite_targets = ["wps.exe", "et.exe", "wpp.exe"]

        # 新增：可执行文件定位（缓存→注册表→常见目录→快捷方式→PATH + 特例）
        def _find_executable(name: str) -> Optional[str]:
            try:
                from pathlib import Path
                import json
                nm = (name or "").strip()
                if not nm:
                    return None

                # 支持别名（如 QQNT.exe）
                alt_names = [nm]
                if nm.lower() == "qq.exe":
                    alt_names.append("QQNT.exe")
                ln_lower = {n.lower() for n in alt_names}

                # 缓存读写
                def _cache_store(p: Path) -> None:
                    try:
                        cache_path = getattr(Config, "APP_PATH_CACHE_FILE", Config.OUTPUT_DIR / "app_paths.json")
                        cache_dir = Path(cache_path).parent
                        cache_dir.mkdir(parents=True, exist_ok=True)
                        data = {}
                        if Path(cache_path).exists():
                            with open(cache_path, "r", encoding="utf-8") as f:
                                try:
                                    data = json.load(f) or {}
                                except Exception:
                                    data = {}
                        for k in alt_names:
                            data[k] = str(p)
                            data[k.lower()] = str(p)
                        with open(cache_path, "w", encoding="utf-8") as f:
                            json.dump(data, f, ensure_ascii=False, indent=2)
                    except Exception:
                        pass

                # 0) 缓存命中
                try:
                    cache_path = getattr(Config, "APP_PATH_CACHE_FILE", Config.OUTPUT_DIR / "app_paths.json")
                    if Path(cache_path).exists():
                        with open(cache_path, "r", encoding="utf-8") as f:
                            cache = json.load(f)
                        for key in [nm, nm.lower()]:
                            pstr = cache.get(key)
                            if pstr:
                                p = Path(pstr)
                                if p.exists():
                                    try:
                                        log_event("APP_PATH_CACHE_HIT", f"name={nm}, path={p}")
                                    except Exception:
                                        pass
                                    return str(p)
                except Exception:
                    pass

                # 1) 注册表优先查找（App Paths / Classes / Uninstall）
                try:
                    import winreg
                    # App Paths（遍历备用名称）
                    for hive in (winreg.HKEY_CURRENT_USER, winreg.HKEY_LOCAL_MACHINE):
                        for wow in (0, winreg.KEY_WOW64_32KEY, winreg.KEY_WOW64_64KEY):
                            for nm2 in alt_names:
                                try:
                                    key = winreg.OpenKey(
                                        hive,
                                        rf"Software\\Microsoft\\Windows\\CurrentVersion\\App Paths\\{nm2}",
                                        0,
                                        winreg.KEY_READ | wow,
                                    )
                                    val, _ = winreg.QueryValueEx(key, "")
                                    if val:
                                        p = Path(val)
                                        if p.exists():
                                            _cache_store(p)
                                            return str(p)
                                except OSError:
                                    pass
                    # Classes: Applications\<exe>\shell\open\command（遍历备用名称）
                    for hive in (winreg.HKEY_CLASSES_ROOT, winreg.HKEY_LOCAL_MACHINE):
                        for wow in (0, winreg.KEY_WOW64_32KEY, winreg.KEY_WOW64_64KEY):
                            for nm2 in alt_names:
                                try:
                                    key = winreg.OpenKey(
                                        hive,
                                        rf"Software\\Classes\\Applications\\{nm2}\\shell\\open\\command",
                                        0,
                                        winreg.KEY_READ | wow,
                                    )
                                    cmd, _ = winreg.QueryValueEx(key, "")
                                    if cmd:
                                        exe_candidate = None
                                        if '"' in cmd:
                                            m = re.search(r'"([^\"]+\\.exe)"', cmd, flags=re.IGNORECASE)
                                            if m:
                                                exe_candidate = m.group(1)
                                        if not exe_candidate:
                                            for token in re.split(r"\s+", cmd.strip()):
                                                if token.lower().endswith(".exe"):
                                                    exe_candidate = token
                                                    break
                                        if exe_candidate:
                                            p = Path(exe_candidate)
                                            if p.exists():
                                                _cache_store(p)
                                                return str(p)
                                except OSError:
                                    pass
                    # Uninstall: DisplayIcon / InstallLocation
                    for hive in (winreg.HKEY_CURRENT_USER, winreg.HKEY_LOCAL_MACHINE):
                        for wow in (0, winreg.KEY_WOW64_32KEY, winreg.KEY_WOW64_64KEY):
                            try:
                                base = winreg.OpenKey(
                                    hive,
                                    r"Software\\Microsoft\\Windows\\CurrentVersion\\Uninstall",
                                    0,
                                    winreg.KEY_READ | wow,
                                )
                                i = 0
                                while True:
                                    try:
                                        subkey_name = winreg.EnumKey(base, i)
                                        sk = winreg.OpenKey(base, subkey_name, 0, winreg.KEY_READ | wow)
                                        display_icon = None
                                        install_loc = None
                                        try:
                                            display_icon, _ = winreg.QueryValueEx(sk, "DisplayIcon")
                                        except OSError:
                                            pass
                                        try:
                                            install_loc, _ = winreg.QueryValueEx(sk, "InstallLocation")
                                        except OSError:
                                            pass
                                        def _strip_exe(s: str) -> Optional[str]:
                                            if not s:
                                                return None
                                            s = str(s).strip().strip('"')
                                            m = re.search(r'([^\"]+\\.exe)', s, flags=re.IGNORECASE)
                                            return m.group(1) if m else None
                                        if display_icon:
                                            ep = _strip_exe(display_icon)
                                            if ep:
                                                p = Path(ep)
                                                if p.exists() and (p.name.lower() in ln_lower or p.name in alt_names):
                                                    _cache_store(p)
                                                    return str(p)
                                        if install_loc:
                                            for nm2 in alt_names:
                                                p = Path(install_loc) / nm2
                                                if p.exists():
                                                    _cache_store(p)
                                                    return str(p)
                                        i += 1
                                    except OSError:
                                        break
                            except OSError:
                                pass
                except Exception:
                    pass

                # 2) 常见目录浅层扫描（深度 ≤ 3）
                try:
                    from collections import deque
                    roots = []
                    try:
                        import string
                        for letter in string.ascii_uppercase:
                            drive = Path(f"{letter}:/")
                            if drive.exists():
                                for sub in ["Program Files","Program Files (x86)","ProgramData","Tencent","Apps","Applications"]:
                                    p = drive / sub
                                    if p.exists():
                                        roots.append(p)
                    except Exception:
                        pass
                    roots += [
                        Path.home() / "AppData" / "Local",
                        Path.home() / "AppData" / "Roaming",
                    ]
                    for root in roots:
                        if not root.exists():
                            continue
                        dq = deque([(root, 0)])
                        while dq:
                            d, depth = dq.popleft()
                            if depth > 3:
                                continue
                            try:
                                for entry in d.iterdir():
                                    if entry.is_file() and (entry.name.lower() in ln_lower or entry.name in alt_names):
                                        _cache_store(entry)
                                        return str(entry)
                                    elif entry.is_dir() and depth < 3:
                                        dq.append((entry, depth + 1))
                            except Exception:
                                pass
                except Exception:
                    pass

                # 2) 解析快捷方式（桌面/开始菜单）
                try:
                    import win32com.client  # type: ignore
                    shell = win32com.client.Dispatch("WScript.Shell")
                    lnk_dirs = [
                        Path.home() / "Desktop",
                        Path("C:/ProgramData/Microsoft/Windows/Start Menu/Programs"),
                        Path.home() / "AppData/Roaming/Microsoft/Windows/Start Menu/Programs",
                    ]
                    for d in lnk_dirs:
                        if not d.exists():
                            continue
                        candidates = list(d.glob("*.lnk")) + list(d.glob("*/*.lnk"))
                        for lnk in candidates:
                            try:
                                sc = shell.CreateShortcut(str(lnk))
                                target = sc.TargetPath
                                if target:
                                    p = Path(target)
                                    if p.exists() and (p.name.lower() in ln_lower or p.name in alt_names):
                                        _cache_store(p)
                                        return str(p)
                            except Exception:
                                pass
                except Exception:
                    pass

                # 3) PATH 环境变量
                try:
                    import os
                    for dir_path in os.environ.get("PATH", "").split(";"):
                        dir_path = dir_path.strip()
                        if not dir_path:
                            continue
                        dp = Path(dir_path)
                        for nm2 in alt_names:
                            candidate = dp / nm2
                            if candidate.exists():
                                _cache_store(candidate)
                                return str(candidate)
                except Exception:
                    pass

                # 4) 特例：已知应用的常见安装位置（保留原逻辑）
                special = {
                    "WeChat.exe": [
                        Path("C:/Program Files/Tencent/WeChat/WeChat.exe"),
                        Path("C:/Program Files (x86)/Tencent/WeChat/WeChat.exe"),
                        Path.home() / "AppData/Local/Tencent/WeChat/WeChat.exe",
                    ],
                    "Weixin.exe": [
                        Path("C:/Program Files/Tencent/WeChat/Weixin.exe"),
                        Path("C:/Program Files (x86)/Tencent/WeChat/Weixin.exe"),
                        Path.home() / "AppData/Local/Tencent/WeChat/Weixin.exe",
                    ],
                    "QQ.exe": [
                        Path("C:/Program Files/Tencent/QQNT/QQ.exe"),
                        Path("C:/Program Files (x86)/Tencent/QQ/QQ.exe"),
                        Path("C:/Program Files/Tencent/QQ/QQ.exe"),
                        Path("C:/Program Files (x86)/Tencent/QQ/Bin/QQ.exe"),
                        Path("C:/Program Files/Tencent/QQ/Bin/QQ.exe"),
                        Path.home() / "AppData/Local/Tencent/QQ/QQ.exe",
                    ],
                    "QQNT.exe": [
                        Path("C:/Program Files/Tencent/QQNT/QQNT.exe"),
                        Path.home() / "AppData/Local/Programs/qqnt/QQNT.exe",
                    ],
                }
                for key in alt_names:
                    for c in special.get(key, []):
                        if c.exists():
                            _cache_store(c)
                            return str(c)

            except Exception:
                pass
            return None

        if op == "open":
            try:
                # 套件默认打开主应用
                target_open = "wps.exe" if suite_targets else exe
                # 如果已运行则不重复启动
                try:
                    if isinstance(target_open, str) and _is_running(target_open):
                        return True, f"应用已在运行: {matched_alias or primary}"
                except Exception:
                    pass
                started = False
                abs_path = None
                try:
                    os.startfile(target_open)
                    started = True
                except Exception:
                        # 尝试定位可执行文件的绝对路径
                        if isinstance(target_open, str) and target_open.lower().endswith(".exe"):
                            abs_path = _find_executable(target_open)
                            # 针对 QQ.exe 增加 QQNT.exe 兜底
                            if not abs_path and isinstance(target_open, str) and target_open.lower() == "qq.exe":
                                alt = _find_executable("QQNT.exe")
                                if alt:
                                    abs_path = alt
                            if not abs_path:
                                # 允许全盘扫描
                                try:
                                    if getattr(Config, "ENABLE_FULL_DRIVE_SCAN", False):
                                        import string
                                        from collections import deque
                                        names = [target_open] if isinstance(target_open, str) else []
                                        if isinstance(target_open, str) and target_open.lower() == "qq.exe":
                                            names.append("QQNT.exe")
                                        lower = {n.lower() for n in names}
                                        for letter in string.ascii_uppercase:
                                            drive = Path(f"{letter}:/")
                                            if not drive.exists():
                                                continue
                                            dq = deque([(drive, 0)])
                                            while dq and not abs_path:
                                                d, depth = dq.popleft()
                                                if depth > 4:
                                                    continue
                                                try:
                                                    for entry in d.iterdir():
                                                        if entry.is_file() and entry.name.lower() in lower:
                                                            abs_path = str(entry)
                                                            break
                                                        elif entry.is_dir() and depth < 4:
                                                            dq.append((entry, depth + 1))
                                                except Exception:
                                                    pass
                                except Exception:
                                    pass
                            try:
                                log_event("APP_EXE_SEARCH", f"name={target_open}, found={bool(abs_path)}")
                            except Exception:
                                pass
                        if abs_path:
                            try:
                                from pathlib import Path
                                import json
                                cache_path = getattr(Config, "APP_PATH_CACHE_FILE", Config.OUTPUT_DIR / "app_paths.json")
                                cache_dir = Path(cache_path).parent
                                cache_dir.mkdir(parents=True, exist_ok=True)
                                data = {}
                                if Path(cache_path).exists():
                                    with open(cache_path, "r", encoding="utf-8") as f:
                                        try:
                                            data = json.load(f) or {}
                                        except Exception:
                                            data = {}
                                keys = [target_open] if isinstance(target_open, str) else []
                                if isinstance(target_open, str) and target_open.lower() == "qq.exe":
                                    keys += ["QQNT.exe"]
                                for k in keys:
                                    data[k] = abs_path
                                    data[k.lower()] = abs_path
                                with open(cache_path, "w", encoding="utf-8") as f:
                                    json.dump(data, f, ensure_ascii=False, indent=2)
                            except Exception:
                                pass
                            subprocess.Popen([abs_path], shell=False)
                            started = True
                        else:
                            # 直接尝试别名兜底启动
                            if isinstance(target_open, str) and target_open.lower() == "qq.exe":
                                try:
                                    os.startfile("QQNT.exe")
                                    started = True
                                except Exception:
                                    pass
                                if not started:
                                    subprocess.Popen([target_open], shell=False)
                                    started = True
                # 简易运行监控
                try:
                    alive = _is_running(target_open if isinstance(target_open, str) else str(target_open))
                    log_event("APP_RUN_MONITOR", f"name={matched_alias or primary}, alive={alive}")
                except Exception:
                    pass
                # 执行预设流程
                try:
                    presets = (
                        Config.APP_PRESET_ACTIONS.get(primary)
                        or (matched_alias and Config.APP_PRESET_ACTIONS.get(matched_alias))
                        or (isinstance(target_open, str) and Config.APP_PRESET_ACTIONS.get(target_open))
                        or (abs_path and Config.APP_PRESET_ACTIONS.get(os.path.basename(abs_path)))
                        or []
                    )
                    if presets:
                        log_event("APP_PRESET_RUN", f"count={len(presets)}")
                        self.execute_actions(presets)
                except Exception:
                    pass
                return True, f"已打开应用: {matched_alias or primary}"
            except Exception as e:
                return False, f"打开应用失败: {e}"

        if op == "login":
            try:
                from pywinauto import Desktop
                from pywinauto.keyboard import send_keys
                import time
                start = time.time()
                # 新增：先尝试置顶微信窗口，提升视觉命中率
                try:
                    self._execute_system_control(op="focus_window", value=None, target="微信")
                except Exception:
                    pass

                # 加强：登录流程一开始即进行一次“窗口矩形底部中心”直接点击 + ENTER
                try:
                    for backend in ("win32",):
                        try:
                            win_login = Desktop(backend=backend).window(title_re="微信.*|WeChat.*|Weixin.*")
                            win_login.wait("exists ready", timeout=2)
                            rect = None
                            try:
                                rect = win_login.rectangle()
                            except Exception:
                                try:
                                    rect = win_login.wrapper_object().rectangle()
                                except Exception:
                                    rect = None
                            if rect:
                                import pyautogui
                                cx = (rect.left + rect.right) // 2
                                cy = rect.bottom - int((rect.bottom - rect.top) * 0.15)
                                pyautogui.moveTo(cx, cy, duration=0.03)
                                pyautogui.click(cx, cy)
                                try:
                                    send_keys("{ENTER}")
                                except Exception:
                                    pass
                                try:
                                    log_event("WECHAT_LOGIN_CENTER_CLICK_EARLY", f"pos=({cx},{cy})")
                                except Exception:
                                    pass
                                time.sleep(0.4)
                        except Exception:
                            continue
                except Exception:
                    pass

                # 1) UIA优先 + 单次ROI视觉分析：尝试点击登录按钮
                try:
                    candidates = ["进入微信", "登录", "登 录", "进入 WeChat", "进入 微信"]
                    # 尝试通过UIA直接点击按钮
                    try:
                        from pywinauto import Desktop as UIDesktop
                        win_uia = UIDesktop(backend="uia").window(title_re="微信.*|WeChat.*|Weixin.*")
                        win_uia.wait("exists ready", timeout=3)
                        btn = None
                        for patt in ["进入微信", "登录", "登 录"]:
                            try:
                                btn = win_uia.child_window(title_re=f".*{patt}.*", control_type="Button").wrapper_object()
                                btn.click_input()
                                log_event("WECHAT_LOGIN_UIA_CLICK", f"button={patt}")
                                break
                            except Exception:
                                btn = None
                        # 若UIA未命中，则在登录窗口矩形内做一次OCR分析并点击最优候选
                        if btn is None:
                            rect = None
                            try:
                                rect = win_uia.rectangle()
                            except Exception:
                                try:
                                    rect = win_uia.wrapper_object().rectangle()
                                except Exception:
                                    rect = None
                            try:
                                SpeechFeedback().speak("正在登录窗口内进行一次视觉分析并尝试点击。")
                            except Exception:
                                pass
                            if rect:
                                ve = VisionEngine()
                                items = []
                                try:
                                    items = ve.ocr_list_texts_in_roi(rect, limit=80)
                                except Exception:
                                    items = []
                                def _norm(s: str) -> str:
                                    import re
                                    s = (s or "").lower().strip()
                                    s = re.sub(r"\s+", "", s)
                                    return s
                                cand_norms = [_norm(c) for c in ["进入微信", "登录", "登 录"]]
                                best_pair = None
                                best_score = -1.0
                                for txt, bb in items:
                                    tn = _norm(txt)
                                    if any(cn and cn in tn for cn in cand_norms):
                                        area = bb.w * bb.h
                                        score = area + bb.y * 0.2  # 偏好靠下的大按钮
                                        if score > best_score:
                                            best_score = score
                                            best_pair = (txt, bb)
                                if best_pair:
                                    if ve.click_bbox(best_pair[1]):
                                        try:
                                            log_event("WECHAT_LOGIN_ROI_CLICK", f"text={best_pair[0]}")
                                        except Exception:
                                            pass
                    except Exception:
                        pass
                except Exception:
                    pass
                # 新增：保存视觉失败截图，便于定位问题
                try:
                    VisionEngine().save_screen_debug("output/vision_login_fail.png")
                    log_event("VISION_DEBUG_SAVED", "output/vision_login_fail.png")
                except Exception:
                    pass
                # 新增：OpenInterpreter 风格控制器兜底（UIA/视觉混合）
                try:
                    from .computer_control import ComputerController
                    ctrl = ComputerController()
                    for txt in candidates:
                        if ctrl.click_text_or_uia(txt, title_contains="微信"):
                            try:
                                log_event("WECHAT_LOGIN_CTRL_CLICK", f"clicked={txt}")
                            except Exception:
                                pass
                            break
                except Exception:
                    pass
                # 2) 等待主窗口就绪（允许手动扫码/点击登录）
                try:
                    speaker = SpeechFeedback()
                    # 先快速检测主窗口，若已就绪则不播报登录提示
                    try:
                        dt = Desktop(backend="win32")
                        wins = []
                        try:
                            wins = [w for w in dt.windows() if re.search(r"微信|WeChat|Weixin", w.window_text() or "") and w.is_visible()]
                        except Exception:
                            pass
                        win_main = None
                        if wins:
                            # 选择面积最大的可见窗口
                            try:
                                wins_sorted = sorted(wins, key=lambda w: (w.rectangle().width() * w.rectangle().height()), reverse=True)
                                win_main = wins_sorted[0]
                            except Exception:
                                win_main = wins[0]
                        else:
                            # 回退到直接匹配窗口并等待就绪
                            tmp = dt.window(title_re="微信.*|WeChat.*|Weixin.*")
                            tmp.wait("exists ready", timeout=5)
                            win_main = tmp
                        # 聚焦并置顶
                        try:
                            win_main.set_focus()
                        except Exception:
                            try:
                                win_main.wrapper_object().set_focus()
                            except Exception:
                                pass
                        try:
                            import win32gui, win32con
                            hwnd = win_main.handle if hasattr(win_main, "handle") else win_main.wrapper_object().handle
                            win32gui.SetWindowPos(hwnd, win32con.HWND_TOPMOST, 0, 0, 0, 0, win32con.SWP_NOMOVE | win32con.SWP_NOSIZE)
                            log_event("WECHAT_TOPMOST_MAIN_SET", f"hwnd={hwnd}")
                        except Exception:
                            pass
                        try:
                            log_event("WECHAT_LOGIN_SUCCESS", "backend=win32")
                        except Exception:
                            pass
                        try:
                            speaker.speak("检测到微信主窗口，继续执行。")
                        except Exception:
                            pass
                        return True, "已登录并聚焦微信主窗口"
                    except Exception:
                        pass
                    # 若未检测到主窗口，再提示用户扫码/点击登录并进入等待循环
                    try:
                        speaker.speak("请手动扫码或点击登录，我会等待直到进入微信。")
                    except Exception:
                        pass
                    deadline = time.time() + max(getattr(Config, "WECHAT_LOGIN_MAX_SEC", 25), 60)
                    while time.time() < deadline:
                        for backend in ("win32",):
                            try:
                                win_main = Desktop(backend=backend).window(title_re="微信.*|WeChat.*|Weixin.*")
                                win_main.wait("exists ready", timeout=2)
                                try:
                                    win_main.set_focus()
                                except Exception:
                                    try:
                                        win_main.wrapper_object().set_focus()
                                    except Exception:
                                        pass
                                try:
                                    # 保持置顶，确保后续搜索可见
                                    import win32gui, win32con
                                    hwnd = win_main.handle if hasattr(win_main, "handle") else win_main.wrapper_object().handle
                                    win32gui.SetWindowPos(hwnd, win32con.HWND_TOPMOST, 0, 0, 0, 0, win32con.SWP_NOMOVE | win32con.SWP_NOSIZE)
                                    log_event("WECHAT_TOPMOST_MAIN_SET", f"hwnd={hwnd}")
                                except Exception:
                                    pass
                                try:
                                    log_event("WECHAT_LOGIN_SUCCESS", f"backend={backend}")
                                except Exception:
                                    pass
                                try:
                                    speaker.speak("检测到微信主窗口，继续执行。")
                                except Exception:
                                    pass
                                return True, "已登录并聚焦微信主窗口"
                            except Exception:
                                continue
                        try:
                            log_event("WECHAT_LOGIN_WAIT", f"elapsed={int(time.time()-start)}")
                        except Exception:
                            pass
                        time.sleep(1.2)
                except Exception:
                    pass
                if (time.time() - start) > Config.WECHAT_LOGIN_MAX_SEC:
                    return False, "登录超时：请确认微信窗口可见，或手动完成登录。"
                # 2) 等待主窗口就绪（视觉点击可能已触发登录）
                try:
                    for backend in ("win32",):
                        try:
                            win_main = Desktop(backend=backend).window(title_re="微信.*|WeChat.*|Weixin.*")
                            win_main.wait("exists ready", timeout=10)
                            try:
                                win_main.set_focus()
                            except Exception:
                                try:
                                    win_main.wrapper_object().set_focus()
                                except Exception:
                                    pass
                            try:
                                log_event("WECHAT_LOGIN_SUCCESS", f"backend={backend}")
                            except Exception:
                                pass
                            return True, "已登录并聚焦微信主窗口"
                        except Exception:
                            continue
                except Exception:
                    pass
                if (time.time() - start) > Config.WECHAT_LOGIN_MAX_SEC:
                    return False, "登录超时：请确认微信窗口可见，或手动完成登录。"
                # 3) 控件树兜底：枚举窗口，查找登录对话框并点击按钮
                try:
                    found = False
                    win_login = None
                    for backend in ("win32",):
                        try:
                            wins = Desktop(backend=backend).windows()
                        except Exception:
                            continue
                        for w in wins:
                            try:
                                title = None
                                try:
                                    title = w.window_text()
                                except Exception:
                                    title = None
                                if not title:
                                    continue
                                if any(k in title for k in ["微信", "WeChat", "Weixin", "登录", "进入微信"]):
                                    try:
                                        w.wait("exists enabled visible ready", timeout=6)
                                        try:
                                            w.set_focus()
                                        except Exception:
                                            try:
                                                w.wrapper_object().set_focus()
                                            except Exception:
                                                pass
                                        win_login = w
                                        found = True
                                        break
                                    except Exception:
                                        continue
                            except Exception:
                                continue
                        if found:
                            break
                    if (time.time() - start) > Config.WECHAT_LOGIN_MAX_SEC:
                        return False, "登录超时：请确认微信窗口可见，或手动完成登录。"
                    if win_login:
                        # 尝试点击按钮控件
                        try:
                            btn = None
                            for patt in ["进入微信", "登录", "登 录"]:
                                try:
                                    btn = win_login.child_window(title_re=f".*{patt}.*", control_type="Button")
                                    btn = btn.wrapper_object()
                                    btn.click_input()
                                    log_event("WECHAT_LOGIN_UIA_CLICK", f"button={patt}")
                                    break
                                except Exception:
                                    btn = None
                            if btn is None:
                                # 回车兜底
                                try:
                                    send_keys("{ENTER}")
                                    log_event("WECHAT_LOGIN_ENTER", "fallback_enter")
                                except Exception:
                                    pass
                                # 直接点击窗口底部中心（绿色按钮常在此），再回车一次
                                try:
                                    import pyautogui
                                    rect = None
                                    try:
                                        rect = win_login.rectangle()
                                    except Exception:
                                        try:
                                            rect = win_login.wrapper_object().rectangle()
                                        except Exception:
                                            rect = None
                                    if rect:
                                        cx = (rect.left + rect.right) // 2
                                        cy = rect.bottom - int((rect.bottom - rect.top) * 0.15)
                                        pyautogui.moveTo(cx, cy, duration=0.05)
                                        pyautogui.click(cx, cy)
                                        log_event("WECHAT_LOGIN_CENTER_CLICK", f"pos=({cx},{cy})")
                                        try:
                                            send_keys("{ENTER}")
                                        except Exception:
                                            pass
                                except Exception:
                                    pass
                        except Exception:
                            # 控件树点击失败，尝试按回车
                            try:
                                send_keys("{ENTER}")
                            except Exception:
                                pass
                        # 再次等待主窗口
                        for backend in ("win32",):
                            try:
                                win_main = Desktop(backend=backend).window(title_re="微信.*|WeChat.*|Weixin.*")
                                win_main.wait("exists ready", timeout=10)
                                try:
                                    win_main.set_focus()
                                except Exception:
                                    try:
                                        win_main.wrapper_object().set_focus()
                                    except Exception:
                                        pass
                                log_event("WECHAT_LOGIN_SUCCESS", f"backend={backend}")
                                return True, "已登录并聚焦微信主窗口"
                            except Exception:
                                continue
                except Exception:
                    pass
                if (time.time() - start) > Config.WECHAT_LOGIN_MAX_SEC:
                    return False, "登录超时：请确认微信窗口可见，或手动完成登录。"
                # 未能确认主窗口：提示人工扫码或点击
                try:
                    log_event("WECHAT_LOGIN_HINT", "请确认微信登录界面可见，或手动点击登录")
                except Exception:
                    pass
                return False, "未能确认微信主窗口，请确认是否已登录或窗口是否可见。"
            except Exception as e:
                return False, f"登录过程异常：{e}"

        if op == "close":
            # 关闭浏览器时优先释放自动化驱动
            try:
                if primary in ("浏览器", "Edge") or (isinstance(exe, str) and exe.lower().endswith("msedge.exe")):
                    close_driver()
            except Exception:
                pass

            # 新增：窗口级关闭（优先 WM_CLOSE，再进程结束）
            def _collect_window_keywords() -> List[str]:
                keys: List[str] = []
                try:
                    # 来源：用户说法/近似别名/解析后的 exe
                    candidates = [primary]
                    if matched_alias:
                        candidates.append(matched_alias)
                    if isinstance(exe, str):
                        candidates.append(exe)
                    for c in candidates:
                        if not c:
                            continue
                        keys.extend(Config.APP_WINDOW_KEYWORDS.get(c, []))
                except Exception:
                    pass
                # 去重保序
                seen = set()
                uniq: List[str] = []
                for k in keys:
                    if k not in seen:
                        seen.add(k)
                        uniq.append(k)
                return uniq

            def _close_windows_by_keywords(keywords: List[str]) -> int:
                try:
                    from pywinauto import Desktop
                    sent = 0
                    desk = Desktop(backend="uia")
                    for w in desk.windows():
                        t = (getattr(w, "window_text", lambda: "")() or "")
                        t = str(t)
                        if any(k.lower() in t.lower() for k in keywords):
                            try:
                                w.close()
                                sent += 1
                            except Exception:
                                pass
                    return sent
                except Exception:
                    return 0

            win_keys = _collect_window_keywords()
            if win_keys:
                try:
                    log_event("APP_WINDOW_KEYS", f"keys={win_keys}")
                except Exception:
                    pass
                sent = _close_windows_by_keywords(win_keys)
                try:
                    log_event("APP_WINDOW_CLOSE", f"sent={sent}")
                except Exception:
                    pass
                import time
                time.sleep(0.6)

            names = suite_targets if suite_targets else [exe]
            tried: List[str] = []
            for name in names:
                name = name.strip()
                if not name:
                    continue
                tried.append(name)
                # 先正常结束
                _kill(name, force=False)
                # 校验
                alive = _is_running(name)
                log_event("APP_CLOSE_VERIFY", f"name={name}, alive={alive}")
                # 如仍存活则强制结束
                if alive:
                    _kill(name, force=True)
            detail = (
                f"已尝试关闭应用: {matched_alias or primary}"
                + (f"（目标：{', '.join(tried)}）" if tried else "")
            )
            return True, detail

        return False, f"未知应用操作: {op}"

    def _execute_system_control(self, op: str, value: Optional[int], target: Optional[str] = None) -> Tuple[bool, str]:
        op = (op or "").strip().lower()
        if op == "focus_window":
            tgt = (target or "").strip()
            # 收集关键词：优先 target，其次配置别名
            win_keys: List[str] = []
            if tgt:
                win_keys = Config.APP_WINDOW_KEYWORDS.get(tgt, []) or Config.APP_WINDOW_KEYWORDS.get(f"{tgt}.exe", [])
            if not win_keys:
                win_keys = Config.APP_WINDOW_KEYWORDS.get("浏览器", [])  # 轻微兜底，不影响失败时返回
            if not win_keys:
                return False, "未提供有效窗口关键词"
            try:
                from pywinauto import Desktop
                import time
                focused = False
                for kw in win_keys:
                    try:
                        # 优先 UIA，失败则回退 win32 提升兼容性
                        focused_try = False
                        for backend in ("win32",):
                            try:
                                w = Desktop(backend=backend).window(title_re=f".*{re.escape(kw)}.*")
                                w.wait("exists enabled visible ready", timeout=6)
                                try:
                                    w.set_focus()
                                except Exception:
                                    try:
                                        w.wrapper_object().set_focus()
                                    except Exception:
                                        pass
                                focused_try = True
                                break
                            except Exception:
                                continue
                        if focused_try:
                            focused = True
                            break
                    except Exception:
                        continue
                time.sleep(0.4)
                if focused:
                    try:
                        log_event("FOCUS_WINDOW", f"target={tgt}, keys={win_keys}")
                    except Exception:
                        pass
                    return True, f"已置顶窗口：{tgt or (win_keys[0] if win_keys else '')}"
                return False, f"未找到目标窗口：{tgt}"
            except Exception as e:
                return False, f"置顶窗口失败：{e}"

        if op in ("set_volume", "increase_volume", "decrease_volume", "mute", "unmute"):
            try:
                from ctypes import POINTER, cast
                from comtypes import CLSCTX_ALL  # type: ignore
                from pycaw.pycaw import AudioUtilities, IAudioEndpointVolume  # type: ignore
                devices = AudioUtilities.GetSpeakers()
                interface = devices.Activate(IAudioEndpointVolume._iid_, CLSCTX_ALL, None)
                volume = cast(interface, POINTER(IAudioEndpointVolume))
                # scalar in [0.0, 1.0]
                if op == "mute":
                    volume.SetMute(1, None)
                    return True, "已静音"
                if op == "unmute":
                    volume.SetMute(0, None)
                    return True, "已取消静音"
                current = volume.GetMasterVolumeLevelScalar()
                if op == "set_volume" and value is not None:
                    target = max(0.0, min(1.0, (value or 0) / 100.0))
                elif op == "increase_volume":
                    target = max(0.0, min(1.0, current + 0.1))
                elif op == "decrease_volume":
                    target = max(0.0, min(1.0, current - 0.1))
                else:
                    target = current
                volume.SetMasterVolumeLevelScalar(target, None)
                return True, f"音量已设置为 {int(target*100)}%"
            except Exception:
                return False, "音量控制不可用（缺少依赖或权限）"

        if op in ("set_brightness", "increase_brightness", "decrease_brightness"):
            try:
                import screen_brightness_control as sbc  # type: ignore
                current = None
                try:
                    vals = sbc.get_brightness()
                    current = int(vals[0]) if vals else None
                except Exception:
                    pass
                if op == "set_brightness" and value is not None:
                    target = max(0, min(100, value))
                elif op == "increase_brightness":
                    target = max(0, min(100, (current or 50) + 10))
                else:  # decrease
                    target = max(0, min(100, (current or 50) - 10))
                sbc.set_brightness(target)
                return True, f"亮度已设置为 {target}%"
            except Exception:
                return False, "亮度控制不可用（缺少依赖或设备不支持）"

        return False, f"未知系统操作: {op}"

    def execute(self, action: Dict[str, Any]) -> Tuple[bool, str]:
        t = action.get("type")
        if t == "write_article":
            return self.write_article(
                topic=action.get("topic", "主题"),
                words=int(action.get("words", 300)),
                save_path=action.get("save_path", "Desktop"),
            )
        # 其他动作复用批量执行逻辑
        results = self.execute_actions([action])
        res = results[0] if results else {"success": False, "detail": "未执行"}
        return res["success"], res["detail"]

    def _execute_wechat_message(self, contact: str, text: str) -> Tuple[bool, str]:
        contact = (contact or "").strip()
        text = (text or "").strip()
        if not contact:
            return False, "未指定联系人"
        if not text:
            return False, "未指定消息内容"
        try:
            try:
                SpeechFeedback().speak(f"开始向{contact}发送消息。")
            except Exception:
                pass
            # 确保微信已打开（若未打开，前序动作应已处理；此处兜底）
            try:
                self._execute_app_control(op="open", app="微信")
            except Exception:
                pass
            import time
            time.sleep(1.0)
            # 使用 WCFerry 检测登录状态；已登录则置顶主窗口，未登录则执行登录流程
            login_status = None
            try:
                if getattr(Config, "ENABLE_WCFERRY_LOGIN", False):
                    from wcferry import Wcf
                    try:
                        wcf = Wcf(block=False, debug=False)
                    except BaseException:
                        wcf = None
                    if wcf is not None:
                        try:
                            login_status = bool(wcf.is_login())
                        except BaseException:
                            login_status = False
                        try:
                            log_event("WECHAT_WCF_LOGIN", f"is_login={login_status}")
                        except Exception:
                            pass
                        try:
                            wcf.cleanup()
                        except BaseException:
                            pass
                    else:
                        login_status = None
                else:
                    login_status = None
            except BaseException:
                login_status = None
            # 根据登录状态执行
            if login_status:
                # 优先尝试通过 WCFerry 直接发送消息，避免 UI 焦点问题
                try:
                    if getattr(Config, "ENABLE_WCFERRY_SEND", False):
                        from wcferry import Wcf
                        try:
                            wcf2 = Wcf(block=False, debug=False)
                            if bool(wcf2.is_login()):
                                target_wxid = None
                                if contact in ["文件传输助手", "filehelper", "File Transfer", "文件助手"]:
                                    target_wxid = "filehelper"
                                else:
                                    try:
                                        contacts = wcf2.get_contacts() or []
                                        for c in contacts:
                                            nickname = (c.get("nickname") or "").strip()
                                            remark = (c.get("remark") or "").strip()
                                            if contact and (contact in nickname or contact in remark):
                                                target_wxid = c.get("wxid")
                                                break
                                    except BaseException:
                                        target_wxid = None
                                if target_wxid:
                                    try:
                                        wcf2.send_text(text, target_wxid)
                                        try:
                                            log_event("WECHAT_WCF_SEND", f"to={contact}, wxid={target_wxid}, len={len(text)}")
                                        except Exception:
                                            pass
                                        try:
                                            wcf2.cleanup()
                                        except BaseException:
                                            pass
                                        return True, f"已通过WCF向{contact}发送：{text}"
                                    except BaseException:
                                        try:
                                            wcf2.cleanup()
                                        except BaseException:
                                            pass
                        except BaseException:
                            pass
                except Exception:
                    pass
                # 如未通过 WCF 发送成功，则聚焦微信窗口，继续 UI 兜底
                try:
                    from pywinauto import Desktop
                    wins = Desktop(backend="win32").windows()
                    for w in wins:
                        try:
                            t = (getattr(w, "window_text", lambda: "")() or "")
                        except Exception:
                            t = ""
                        if ("微信" in t) or ("WeChat" in t):
                            try:
                                w.set_focus()
                            except Exception:
                                pass
                            # 置顶微信窗口，确保位于最上层
                            try:
                                import win32gui, win32con  # type: ignore
                                hwnd = getattr(w, "handle", None)
                                if hwnd:
                                    win32gui.SetWindowPos(hwnd, win32con.HWND_TOPMOST, 0, 0, 0, 0,
                                                         win32con.SWP_NOMOVE | win32con.SWP_NOSIZE)
                                    try:
                                        log_event("WECHAT_TOPMOST_SET", f"hwnd={hwnd}")
                                    except Exception:
                                        pass
                            except Exception as e:
                                try:
                                    log_event("WECHAT_TOPMOST_FAIL", str(e))
                                except Exception:
                                    pass
                            break
                except Exception:
                    pass
            else:
                try:
                    self._execute_app_control(op="login", app="微信")
                except Exception:
                    pass
            # 优先尝试视觉入口点击联系人并输入消息（带语音播报）
            try:
                try:
                    SpeechFeedback().speak("尝试视觉定位联系人并直接发送。")
                except Exception:
                    pass
                ok_visual = execute_visual_action(f"点击文本: {contact}", input_text=text, press_enter=True, speak=True)
                if ok_visual:
                    try:
                        log_event("WECHAT_MESSAGE_VISUAL", f"contact={contact}, text_len={len(text)}")
                    except Exception:
                        pass
                    return True, f"已通过视觉入口向{contact}发送：{text}"
            except Exception:
                pass
            # 新增：保存视觉失败截图，便于定位问题
            try:
                VisionEngine().save_screen_debug("output/vision_contact_fail.png")
                log_event("VISION_DEBUG_SAVED", "output/vision_contact_fail.png")
            except Exception:
                pass
            # 新增：OpenInterpreter 风格控制器尝试（点击联系人并发送）
            try:
                from .computer_control import ComputerController
                ctrl = ComputerController()
                try:
                    SpeechFeedback().speak("视觉失败，尝试控制器点击联系人。")
                except Exception:
                    pass
                if ctrl.click_text_or_uia(contact, title_contains="微信"):
                    try:
                        SpeechFeedback().speak("已进入聊天，尝试发送消息。")
                    except Exception:
                        pass
                    if ctrl.send_message_in_window("微信", text):
                        try:
                            log_event("WECHAT_MESSAGE_CTRL", f"contact={contact}, text_len={len(text)}")
                        except Exception:
                            pass
                        try:
                            SpeechFeedback().speak("控制器路径发送成功。")
                        except Exception:
                            pass
                        return True, f"已通过控制器向{contact}发送：{text}"
            except Exception:
                pass
            # 新增：控制器搜索框兜底（UIA 搜索并选择联系人）
            try:
                from .computer_control import ComputerController
                ctrl = ComputerController()
                search_terms = [contact]
                try:
                    use_syn = getattr(Config, "ENABLE_WECHAT_CONTACT_SYNONYMS", False)
                except Exception:
                    use_syn = False
                if use_syn and contact == "文件传输助手":
                    search_terms = ["文件传输助手", "File Transfer", "filehelper", "文件助手"]
                try:
                    SpeechFeedback().speak("尝试使用搜索框进入联系人并发送。")
                except Exception:
                    pass
                if ctrl.search_and_select("微信", search_terms, verify_text=contact):
                    if ctrl.send_message_in_window("微信", text):
                        try:
                            log_event("WECHAT_MESSAGE_CTRL_SEARCH", f"contact={contact}, text_len={len(text)}")
                        except Exception:
                            pass
                        try:
                            SpeechFeedback().speak("搜索兜底路径发送成功。")
                        except Exception:
                            pass
                        return True, f"已通过搜索框兜底向{contact}发送：{text}"
            except Exception:
                pass
            # 使用 win32 后端连接微信窗口（更稳定且避免 UIA/COM 阻塞）
            try:
                from pywinauto import Desktop
                from pywinauto.keyboard import send_keys
            except Exception as e:
                return False, f"缺少依赖：{e}（请安装 pywinauto/pywin32）"
            try:
                # 遍历顶层窗口，选择标题匹配且面积最大的作为主窗口
                wins = Desktop(backend="win32").windows()
                candidates = []
                for w in wins:
                    try:
                        title = None
                        try:
                            title = w.window_text()
                        except Exception:
                            title = None
                        if not title:
                            continue
                        if re.search(r"(微信|WeChat|Weixin)", title):
                            try:
                                rect = w.rectangle()
                                area = max(1, (rect.right - rect.left) * (rect.bottom - rect.top))
                                candidates.append((area, w))
                            except Exception:
                                candidates.append((1, w))
                    except Exception:
                        continue
                if not candidates:
                    return False, "未找到微信窗口：未检测到匹配标题"
                win = sorted(candidates, key=lambda x: x[0], reverse=True)[0][1]
                try:
                    if hasattr(win, "wait"):
                        win.wait("exists enabled visible ready", timeout=8)
                except Exception:
                    pass
                try:
                    win.set_focus()
                except Exception:
                    try:
                        win.wrapper_object().set_focus()
                    except Exception:
                        pass
                # 置顶微信主窗口，确保位于其他窗口之上
                try:
                    import win32gui, win32con  # type: ignore
                    hwnd = getattr(win, "handle", None)
                    if hwnd:
                        win32gui.SetWindowPos(hwnd, win32con.HWND_TOPMOST, 0, 0, 0, 0,
                                             win32con.SWP_NOMOVE | win32con.SWP_NOSIZE)
                        try:
                            log_event("WECHAT_TOPMOST_MAIN_SET", f"hwnd={hwnd}")
                        except Exception:
                            pass
                except Exception as e:
                    try:
                        log_event("WECHAT_TOPMOST_MAIN_FAIL", str(e))
                    except Exception:
                        pass
            except Exception as e:
                return False, f"未找到微信窗口：{e}"
            # 步骤化执行：进入联系人会话 → 发送文本，都带视觉校验与重试
            ve = VisionEngine()
            # 1) 进入目标会话
            def precheck_contact() -> bool:
                try:
                    return bool(ve.find_text_bbox(contact))
                except Exception:
                    return False
            def do_enter_contact() -> None:
                try:
                    # 先确保主窗口获得焦点
                    try:
                        win.set_focus()
                    except Exception:
                        pass
                    time.sleep(0.3)
                    
                    # 清空搜索框并搜索联系人
                    log_event("WECHAT_SEARCH_START", f"searching for contact: {contact}")
                    send_keys("^f")
                    time.sleep(0.8)
                    send_keys("^a")  # 全选现有内容
                    time.sleep(0.2)
                    
                    try:
                        from pywinauto import clipboard
                        # 清洗搜索词为单行，避免换行导致焦点错位
                        _clean_contact = (contact or "").replace("\r", "").replace("\n", "")
                        clipboard.SetText(_clean_contact)
                        send_keys("^v")
                        # 退出中文候选，确保搜索词落入输入框
                        try:
                            send_keys("{ESC}")
                        except Exception:
                            pass
                    except Exception:
                        send_keys(contact)
                    time.sleep(1.0)  # 增加等待时间让搜索结果出现
                    
                    # Tab到“聊天”分组后再回车，避免误入订阅号/公众号
                    try:
                        send_keys("{TAB}")
                        log_event("WECHAT_SEARCH_TAB_CHAT", "tab to chat group")
                    except Exception:
                        pass
                    send_keys("{ENTER}")
                    time.sleep(1.2)  # 增加等待时间让聊天窗口加载
                    
                    # 如果还没进入，尝试按下箭头选择第一个结果再回车
                    send_keys("{DOWN}")
                    time.sleep(0.3)
                    send_keys("{ENTER}")
                    time.sleep(0.8)
                    
                    # 视觉点击联系人作为补救
                    try:
                        execute_visual_action(f"点击文本: {contact}", speak=False)
                    except Exception:
                        pass
                except Exception:
                    pass
            def verify_contact() -> bool:
                # 视觉优先：尝试在屏幕上找到联系人文本
                try:
                    if bool(ve.find_text_bbox(contact)):
                        return True
                except Exception:
                    pass
                # UIA 兜底：在微信主窗口中查找联系人的标题或聊天头部文本
                try:
                    from pywinauto import Desktop as UIDesktop
                    try:
                        win_uia = UIDesktop(backend="uia").window(title_re="微信.*|WeChat.*|Weixin.*")
                        win_uia.wait("exists ready", timeout=3)
                    except Exception:
                        wins = UIDesktop(backend="uia").windows()
                        target = None
                        for w in wins:
                            try:
                                t = (getattr(w, "window_text", lambda: "")() or "")
                            except Exception:
                                t = ""
                            if ("微信" in t) or ("WeChat" in t) or ("Weixin" in t):
                                target = target or w
                                try:
                                    if getattr(w, "is_visible", lambda: True)():
                                        target = w
                                        break
                                except Exception:
                                    pass
                        if target:
                            win_uia = target
                    # 尝试在聊天标题或面包屑中找到联系人名称
                    for ct in ("Text", "Pane", "Group"):
                        try:
                            hdr = win_uia.child_window(title_re=f".*{contact}.*", control_type=ct)
                            if hdr.exists():
                                return True
                        except Exception:
                            continue
                except Exception:
                    pass
                return False
            contact_entered = self._run_visual_step(f"ENTER_{contact}", do_enter_contact, verify_contact, precheck=precheck_contact)
            if not contact_entered:
                # 键盘兜底：不依赖视觉校验，尝试搜索并进入会话
                try:
                    try:
                        win.set_focus()
                    except Exception:
                        pass
                    time.sleep(0.5)
                    
                    # 先尝试按ESC确保在主界面
                    send_keys("{ESC}")
                    time.sleep(0.3)
                    send_keys("{ESC}")
                    time.sleep(0.3)
                    
                    # 窗口存在性检查与重新聚焦（避免在未聚焦或已关闭情况下进行全局搜索）
                    try:
                        win.set_focus()
                    except Exception:
                        try:
                            log_event("WECHAT_MAIN_WINDOW_REFETCH", "attempting reopen after ESC")
                        except Exception:
                            pass
                        try:
                            self._execute_app_control(op="open", app="微信")
                            time.sleep(1.5)
                            from pywinauto import Desktop as ReDesktop
                            wins_refetch = ReDesktop(backend="win32").windows()
                            for w2 in wins_refetch:
                                try:
                                    t2 = (getattr(w2, "window_text", lambda: "")() or "")
                                except Exception:
                                    t2 = ""
                                if ("微信" in t2) or ("WeChat" in t2) or ("Weixin" in t2):
                                    win = w2
                                    try:
                                        win.set_focus()
                                    except Exception:
                                        pass
                                    break
                        except Exception:
                            pass
                    
                    # 如果是文件传输助手，是否启用别名由配置决定
                    search_terms = [contact]
                    try:
                        use_syn = getattr(Config, "ENABLE_WECHAT_CONTACT_SYNONYMS", False)
                    except Exception:
                        use_syn = False
                    if use_syn and contact == "文件传输助手":
                        search_terms = ["文件传输助手", "File Transfer", "filehelper", "文件助手"]
                    
                    # 首先尝试直接的快捷键方法（由配置控制，默认关闭）
                    if getattr(Config, "ENABLE_WECHAT_DIRECT_SHORTCUT", False):
                        log_event("WECHAT_DIRECT_TRY", "trying direct shortcut to file transfer assistant")
                        send_keys("^{F2}")  # 非官方快捷键，可能不兼容
                        time.sleep(2.0)
                        try:
                            if verify_contact():
                                contact_entered = True
                                log_event("WECHAT_DIRECT_SUCCESS", "direct shortcut worked")
                        except Exception:
                            pass
                    
                    if not contact_entered:
                        # 尝试另一种直接方法
                        send_keys("^+f")  # 尝试另一种搜索快捷键
                        time.sleep(1.0)
                        send_keys("文件传输助手")
                        time.sleep(1.0)
                        send_keys("{ENTER}")
                        time.sleep(2.0)
                        try:
                            if verify_contact():
                                contact_entered = True
                                log_event("WECHAT_ALT_SEARCH_SUCCESS", "alternative search worked")
                        except Exception:
                            pass
                    
                    # 多次尝试搜索和进入
                    for search_term in search_terms:
                        log_event("WECHAT_SEARCH_TRY", f"trying search term: {search_term}")
                        
                        # 尝试多次搜索
                        for attempt in range(3):  # 增加到3次尝试
                            log_event("WECHAT_SEARCH_ATTEMPT", f"attempt {attempt + 1} for term: {search_term}")
                            
                            # 打开搜索框
                            send_keys("^f")
                            time.sleep(1.2)  # 增加等待时间
                            
                            # 清空搜索框
                            send_keys("^a")
                            time.sleep(0.3)
                            send_keys("{DELETE}")
                            time.sleep(0.3)
                            
                            # 输入搜索词
                            try:
                                from pywinauto import clipboard
                                # 清洗为单行，避免\r/\n引起候选或分行
                                _clean_term = (search_term or "").replace("\r", "").replace("\n", "")
                                clipboard.SetText(_clean_term)
                                send_keys("^v")
                                # 退出中文候选，确保输入框接收
                                try:
                                    send_keys("{ESC}")
                                except Exception:
                                    pass
                                log_event("WECHAT_SEARCH_INPUT", f"pasted search term: {_clean_term}")
                            except Exception as e:
                                send_keys(search_term)
                                log_event("WECHAT_SEARCH_INPUT", f"typed search term: {search_term}")
                            time.sleep(1.5)  # 增加等待时间
                            
                            # Tab到“聊天”分组后再回车，避免误入订阅号/公众号
                            try:
                                send_keys("{TAB}")
                                log_event("WECHAT_SEARCH_TAB_CHAT", "tab to chat group")
                            except Exception:
                                pass
                            
                            # 按回车搜索
                            send_keys("{ENTER}")
                            time.sleep(2.0)  # 增加等待时间让搜索结果显示
                            
                            # 检查是否成功进入
                            try:
                                if verify_contact():
                                    contact_entered = True
                                    log_event("WECHAT_SEARCH_SUCCESS", f"successfully entered with term: {search_term}")
                                    break
                            except Exception as e:
                                log_event("WECHAT_SEARCH_VERIFY_FAIL", f"verify failed: {str(e)}")
                            
                            # 如果直接回车失败，尝试用方向键选择第一个结果
                            if attempt < 2:  # 前两次尝试使用方向键
                                log_event("WECHAT_SEARCH_ARROW", "trying arrow key selection")
                                send_keys("{DOWN}")
                                time.sleep(0.5)
                                send_keys("{ENTER}")
                                time.sleep(2.0)
                                try:
                                    if verify_contact():
                                        contact_entered = True
                                        log_event("WECHAT_SEARCH_SUCCESS", f"successfully entered with term: {search_term} (arrow key)")
                                        break
                                except Exception as e:
                                    log_event("WECHAT_SEARCH_ARROW_FAIL", f"arrow key failed: {str(e)}")
                            
                            # 如果还是失败，按ESC关闭搜索框重试
                            send_keys("{ESC}")
                            time.sleep(0.5)
                        
                        if contact_entered:
                            break
                    # 新增：UIA 搜索框兜底（直接定位搜索输入框并输入）
                    if not contact_entered:
                        try:
                            from pywinauto import Desktop as UIDesktop
                            try:
                                win_uia = UIDesktop(backend="uia").window(title_re="微信.*|WeChat.*|Weixin.*")
                                win_uia.wait("exists ready", timeout=5)
                            except Exception:
                                wins = UIDesktop(backend="uia").windows()
                                target = None
                                for w in wins:
                                    try:
                                        t = (getattr(w, "window_text", lambda: "")() or "")
                                    except Exception:
                                        t = ""
                                    if ("微信" in t) or ("WeChat" in t) or ("Weixin" in t):
                                        target = target or w
                                        try:
                                            if getattr(w, "is_visible", lambda: True)():
                                                target = w
                                                break
                                        except Exception:
                                            pass
                                if target:
                                    win_uia = target
                            
                            edit = None
                            for patt in ["搜索", "Search", "查找"]:
                                try:
                                    edit = win_uia.child_window(title_re=f".*{patt}.*", control_type="Edit").wrapper_object()
                                    break
                                except Exception:
                                    edit = None
                            if edit is None:
                                try:
                                    edit = win_uia.child_window(control_type="Edit").wrapper_object()
                                except Exception:
                                    edit = None
                            if edit:
                                edit.click_input()
                                time.sleep(0.6)
                                for search_term in search_terms:
                                    try:
                                        from pywinauto import clipboard
                                        clipboard.SetText(search_term)
                                        send_keys("^a")
                                        time.sleep(0.2)
                                        send_keys("{DELETE}")
                                        time.sleep(0.2)
                                        send_keys("^v")
                                        # 退出中文候选，确保搜索词落入输入框
                                        try:
                                            send_keys("{ESC}")
                                        except Exception:
                                            pass
                                    except Exception:
                                        send_keys("^a")
                                        time.sleep(0.2)
                                        send_keys("{DELETE}")
                                        time.sleep(0.2)
                                        send_keys(search_term)
                                    time.sleep(1.0)
                                    
                                    # Tab到“聊天”分组后再回车，避免误入订阅号/公众号
                                    try:
                                        send_keys("{TAB}")
                                        log_event("WECHAT_SEARCH_TAB_CHAT", "tab to chat group")
                                    except Exception:
                                        pass
                                    
                                    send_keys("{ENTER}")
                                    time.sleep(1.5)
                                    try:
                                        if verify_contact():
                                            contact_entered = True
                                            log_event("WECHAT_SEARCH_UIA_SUCCESS", f"entered with term: {search_term}")
                                            break
                                    except Exception:
                                        pass
                        except Exception as e:
                            try:
                                log_event("WECHAT_SEARCH_UIA_FAIL", f"{str(e)}")
                            except Exception:
                                pass
                except Exception:
                    pass
            
            # 新增：限定区域OCR在左侧列表内查找并点击联系人
            if not contact_entered:
                try:
                    from pywinauto import Desktop as UIDesktop
                    try:
                        win_uia = UIDesktop(backend="uia").window(title_re="微信.*|WeChat.*|Weixin.*")
                        win_uia.wait("exists ready", timeout=5)
                    except Exception:
                        wins = UIDesktop(backend="uia").windows()
                        target = None
                        for w in wins:
                            try:
                                t = (getattr(w, "window_text", lambda: "")() or "")
                            except Exception:
                                t = ""
                            if ("微信" in t) or ("WeChat" in t) or ("Weixin" in t):
                                target = target or w
                                try:
                                    if getattr(w, "is_visible", lambda: True)():
                                        target = w
                                        break
                                except Exception:
                                    pass
                        if target:
                            win_uia = target
                    # 获取左侧列表容器矩形
                    list_container = None
                    for ct in ("List", "Pane"):
                        try:
                            cand = win_uia.child_window(control_type=ct)
                            if getattr(cand, "exists", lambda: False)():
                                list_container = cand.wrapper_object()
                                break
                        except Exception:
                            pass
                    rect = None
                    if list_container:
                        try:
                            rect = list_container.rectangle()
                        except Exception:
                            try:
                                rect = list_container.element_info.rectangle
                            except Exception:
                                rect = None
                    if rect:
                        items = []
                        try:
                            items = ve.ocr_list_texts_in_roi(rect, limit=120)
                        except Exception:
                            items = []
                        # 匹配候选联系人名
                        match_terms = [contact]
                        try:
                            if getattr(Config, "ENABLE_WECHAT_CONTACT_SYNONYMS", False) and contact == "文件传输助手":
                                match_terms = ["文件传输助手", "File Transfer", "filehelper", "文件助手"]
                        except Exception:
                            pass
                        target_bb = None
                        target_txt = ""
                        for txt, bb in items:
                            name = (txt or "").strip()
                            if any(t and (t in name) for t in match_terms):
                                target_bb = bb
                                target_txt = name
                                break
                        if target_bb:
                            try:
                                if ve.click_bbox(target_bb):
                                    time.sleep(1.2)
                                    try:
                                        if verify_contact():
                                            contact_entered = True
                                            log_event("WECHAT_OCR_LIST_CLICK", f"clicked={target_txt}")
                                    except Exception:
                                        pass
                            except Exception:
                                pass
                except Exception:
                    pass
            
            # 新增：UIA 列表项兜底（遍历联系人/会话列表，直接点击）
            if not contact_entered:
                try:
                    from pywinauto import Desktop as UIDesktop
                    try:
                        win_uia = UIDesktop(backend="uia").window(title_re="微信.*|WeChat.*|Weixin.*")
                        win_uia.wait("exists ready", timeout=5)
                    except Exception:
                        wins = UIDesktop(backend="uia").windows()
                        target = None
                        for w in wins:
                            try:
                                t = (getattr(w, "window_text", lambda: "")() or "")
                            except Exception:
                                t = ""
                            if ("微信" in t) or ("WeChat" in t) or ("Weixin" in t):
                                target = target or w
                                try:
                                    if getattr(w, "is_visible", lambda: True)():
                                        target = w
                                        break
                                except Exception:
                                    pass
                        if target:
                            win_uia = target
                    items = []
                    for ct in ("ListItem", "TreeItem", "DataItem"):
                        try:
                            items.extend(win_uia.descendants(control_type=ct))
                        except Exception:
                            pass
                    for item in items:
                        name = ""
                        try:
                            name = (item.window_text() or "").strip()
                        except Exception:
                            try:
                                name = (getattr(item, "element_info", None).name or "").strip()
                            except Exception:
                                name = ""
                        if not name:
                            continue
                        # 名称包含联系人或候选搜索词
                        try:
                            match_terms = [contact] + search_terms
                        except Exception:
                            match_terms = [contact]
                        if any(t and (t in name) for t in match_terms):
                            try:
                                item.click_input()
                                time.sleep(1.2)
                                try:
                                    if verify_contact():
                                        contact_entered = True
                                        log_event("WECHAT_LISTITEM_CLICK", f"clicked={name}")
                                        break
                                except Exception:
                                    pass
                            except Exception:
                                continue
                except Exception:
                    pass
            
            # 检查是否成功进入联系人聊天窗口
            if not contact_entered:
                return False, f"无法进入{contact}的聊天窗口，请确认联系人存在且微信窗口可见"
            
            # 2) 发送消息（尝试 Enter 与 Ctrl+Enter），每次后做视觉确认
            def do_send_message() -> None:
                # 发送前尝试聚焦右侧消息输入框，防止粘贴到搜索框或列表
                try:
                    from pywinauto import Desktop as UIDesktop
                    try:
                        win_uia = UIDesktop(backend="uia").window(title_re="微信.*|WeChat.*|Weixin.*")
                        win_uia.wait("exists ready", timeout=3)
                    except Exception:
                        wins = UIDesktop(backend="uia").windows()
                        target = None
                        for w in wins:
                            try:
                                t = (getattr(w, "window_text", lambda: "")() or "")
                            except Exception:
                                t = ""
                            if ("微信" in t) or ("WeChat" in t) or ("Weixin" in t):
                                target = target or w
                                try:
                                    if getattr(w, "is_visible", lambda: True)():
                                        target = w
                                        break
                                except Exception:
                                    pass
                        if target:
                            win_uia = target
                    edit_target = None
                    try:
                        edits = []
                        try:
                            edits = win_uia.descendants(control_type="Edit")
                        except Exception:
                            edits = []
                        # 选择一个可见的输入框
                        for ed in edits:
                            try:
                                if getattr(ed, "is_visible", lambda: True)():
                                    edit_target = ed.wrapper_object()
                                    break
                            except Exception:
                                continue
                        if edit_target:
                            edit_target.click_input()
                            log_event("WECHAT_FOCUS_INPUT", "focused Edit before sending")
                            import time as _t
                            _t.sleep(0.2)
                    except Exception:
                        pass
                except Exception:
                    pass
                try:
                    from pywinauto import clipboard
                    clipboard.SetText(text)
                    send_keys("^v")
                except Exception:
                    send_keys(text)
                time.sleep(0.3)
                try:
                    send_keys("{ENTER}")
                except Exception:
                    pass
                time.sleep(0.5)
                # 再尝试 Ctrl+Enter 保证兼容
                try:
                    from pywinauto import clipboard
                    clipboard.SetText(text)
                    send_keys("^v")
                except Exception:
                    send_keys(text)
                time.sleep(0.3)
                try:
                    send_keys("^{ENTER}")
                except Exception:
                    pass
            def verify_message() -> bool:
                try:
                    return bool(ve.find_text_bbox(text))
                except Exception:
                    return False
            send_success = self._run_visual_step("SEND_TEXT", do_send_message, verify_message)
            if not send_success:
                # 兜底：不依赖视觉确认，直接尝试发送一次
                try:
                    do_send_message()
                    time.sleep(1.0)  # 等待消息发送
                    # 再次验证是否发送成功
                    send_success = verify_message()
                except Exception:
                    pass
            
            # 最终验证：检查联系人与文本是否同时可见
            contact_visible = False
            text_visible = False
            try:
                b_contact = ve.find_text_bbox(contact)
                b_text = ve.find_text_bbox(text)
                contact_visible = bool(b_contact)
                text_visible = bool(b_text)
                log_event("WECHAT_MESSAGE_VERIFY", f"contact_seen={contact_visible}, text_seen={text_visible}")
            except Exception:
                pass
            
            # 只有当消息确实可见时才认为发送成功
            final_success = send_success and text_visible
            try:
                log_event("WECHAT_MESSAGE", f"contact={contact}, text_len={len(text)}, verified={final_success}")
            except Exception:
                pass
            
            if final_success:
                return True, f"已向{contact}发送：{text}"
            else:
                return False, f"向{contact}发送消息失败：消息未在聊天窗口中显示"
        except Exception as e:
            return False, f"微信消息发送失败：{e}"


def execute_visual_action(instruction: str, input_text: Optional[str] = None, press_enter: bool = False, speak: bool = True) -> bool:
    """
    Unified entry to perform a single visual action.
    Examples:
      - instruction="发送" -> click UI element containing this text
      - instruction="点击文本: 发送" -> explicitly click text region
      - instruction+input_text -> click target then type into focused field
    """
    try:
        engine = VisionEngine()
        speaker = SpeechFeedback() if speak else None
        target = instruction.strip()
        if not target:
            if speaker:
                speaker.speak("未提供有效的视觉指令。")
            return False
        # 解析明确的点击文本格式
        if target.startswith("点击文本:"):
            target = target.split(":", 1)[1].strip()
        if speaker:
            # 简要播报当前视觉引擎
            try:
                if getattr(engine, "engine", "") == "aliyun":
                    speaker.speak("已启用阿里云视觉定位，开始尝试点击。")
                elif getattr(engine, "ocr", None):
                    speaker.speak("使用本地OCR兜底定位，开始尝试点击。")
                else:
                    speaker.speak("视觉识别不可用，可能无法定位文本。")
            except Exception:
                pass
            speaker.speak(f"尝试点击：{target}")
        ok = engine.click_text(target)
        if not ok:
            if speaker:
                speaker.speak("未能点击到目标文本，请确认窗口是否可见。")
            return False
        if speaker:
            speaker.speak("点击成功。")
        if input_text:
            if speaker:
                speaker.speak("尝试输入测试文本。")
            typed = engine.type_text(input_text, press_enter=press_enter)
            if speaker:
                speaker.speak("输入已尝试完成。" if typed else "输入尝试失败。")
            return typed
        return True
    except Exception as e:
        try:
            if speak:
                SpeechFeedback().speak(f"视觉动作执行异常：{e}")
        except Exception:
            pass
        return False